In [1]:
using TikzPictures
TikzPicture(L"""
\draw (2,0) -- (5,0) -- (5,2) -- (2,2) -- cycle;
\draw[-latex] (0,1) node[left] {Input} -- (2,1);
\node[align=center] at (3.5,1) {Computation};
\draw[-latex] (5,1) -- (7,1) node[right] {Output};
\draw[-latex] (3.5,3) node[above] {Program} -- (3.5,2);
"""; options="very thick, scale=3, transform shape", preamble="""
\\usepackage{newtxmath}
\\renewcommand{\\familydefault}{\\sfdefault}
""")
Out[1]:
Following instructions are fundamental:
These instructions correspond with the symbolic instructions provided by the instruction set of the computer, which we will call low-level language or assembly.
It is all that is needed to perform a general computation in the sense of Turing.
We don't want to write a program in assembly:
i.e. we need a high-level programming language.
A high-level programming language introduces supplementary instructions:
Believe it or not, that's pretty much all there is to it. Every program you've ever used, no matter how complicated, is made of instructions that look more or less like these.
Programming is the process of breaking a large, complex task into smaller and smaller subtasks until the subtasks are simple enough to be performed with sequences of the basic instructions.
As we progress through this course, you will notice that although the particulars of the source code change from one problem area to the next, certain patterns will emerge in the approach we take.
An algorithm is a mechanical (no intelligence required) sequence of instructions for solving a specific problem, eg. Newton's method for root solving. Executing an algorithm is boring, but designing them is interesting, intellectual challenging, and a central part of this course.
The statements in a high-level programming language have to be translated to instructions that the CPU can execute. There are two ways to do this translation: interpreting or compiling.
An interpreter is a program that reads a high level program and does what it says, eg. a Jupyter notebook. In effect, it translates the program line-by-line, alternately reading lines and carrying out commands.
A compiler is a program that reads a high level program and translates it all at once, before executing any of the commands. Often you compile the program as a separate step, and then execute the compiled code later. In this case the high level program is called the source code and the translated program is called the object code or executable.
Historically compiled programs performed better, i.e. ran faster with less memory, than interpreted programs. Because the compilation step and the execution are decoupled, a compiler could work longer and generate better object code. The runtime overhead is also less. An interpreter allows to use runtime information during translation what makes it more flexible and easier to use. A command-line interface (or Jupyter notebook) allows to dynamically execute statements.
Nowadays with Just-In-Time (JIT) compilation we can use the flexibility of an interpreter with the performance of a compiler: the source code is compiled at runtime. We have however the overhead of both an interpreter and a compiler.
We will use 4 programming languages to illustrate the most important programming techniques:
A high-level language is a formal language, i.e. designed by people for a specific application, in our case to express computations.
Formal languages have strict rules about syntax, eg. $\mathrm{H_2O}$ is a syntactically correct chemical name, but $\mathrm{_2Zz}$ is not. Syntax refers to the structure of a program and the rules about that structure.
Syntax rules come in two flavors, pertaining to tokens and structure. Tokens are the basic elements of the language, such as words, numbers, parentheses, commas, and so on. In Python, a statement like
print('Happy New Year for ', 2018)
has 6 tokens: a function name, an open parenthesis, a string, a comma, a number, and a close parenthesis.
It is possible to make errors in the way one constructs tokens, eg.
'Happy New Year for
is not a legal string token.
It is also possible to make errors in the way the tokens are arranged, i.e. the structure of a statement; eg.
print)"Happy New Year for", 2018(
has an illegal structure.
When you read a sentence in a natural language or a statement in a formal language, you have to figure out what the structure of the sentence is. This process is called parsing.
Once you have parsed a sentence, you can figure out what it means, or the semantics of the sentence. Because we all grow up speaking natural languages, it is sometimes hard to adjust to formal languages. The meaning of a computer program is unambiguous and literal, and can be understood entirely by analysis of the tokens and structure.
Formal languages are more dense than natural languages, so it takes longer to read them. Also, the structure is important, so it is not always best to read from top to bottom, left to right. Instead, learn to parse the program in your head, identifying the tokens and interpreting the structure. Finally, the details matter. Small errors in spelling and punctuation, which you can get away with in natural languages, can make a big difference in a formal language.
Program is a complex process, and because it is done by humans, if often leads to errors. Programming errors are called bugs and the process of tracking them down and correcting them is called debugging.
Three kinds of errors can occur in a program: syntax errors, runtime errors, and semantic errors. It is useful to distinguish between them in order to track them down more quickly.
For example, parentheses have to come in matching pairs, so
(1 + 2)
is legal, but
8)
is a syntax error. If there is a syntax error anywhere in your program, a programming language displays an error message and quits, and you will not be able to run the program. During the first few weeks of your programming career, you might spend a lot of time tracking down syntax errors. As you gain experience, you will make fewer errors and find them faster.
The second type of error is a runtime error, so called because the error does not appear until after the program has started running. These errors are also called exceptions because they usually indicate that something exceptional (and bad) has happened.
Runtime errors are rare in the simple programs you will see in the first few lectures, so it might be a while before you encounter one.
The third type of error is semantic, which means related to meaning. If there is a semantic error in your program, it will run without generating error messages, but it will not do the right thing. It will do something else. Specifically, it will do what you told it to do.
Identifying semantic errors can be tricky because it requires you to work backward by looking at the output of the program and trying to figure out what it is doing.
Programming, and especially debugging, sometimes brings out strong emotions. If you are struggling with a difficult bug, you might feel angry, despondent, or embarrassed.
Preparing for these reactions might help you deal with them. One approach is to think of the computer as an employee with certain strengths, like speed and precision, and particular weaknesses, like lack of empathy and inability to grasp the big picture.
Your job is to be a good manager: find ways to take advantage of the strengths and mitigate the weaknesses. And find ways to use your emotions to engage with the problem, without letting your reactions interfere with your ability to work effectively.
Learning to debug can be frustrating, but it is a valuable skill that is useful for many activities beyond programming.
Top-down design is the process of starting with a large task and breaking it down into smaller, more easily understandable pieces (subtask) that perform a portion of the desired task. Each subtask may in turn be subdivided into smaller subtasks if necessary. Once the program is divided into small pieces, each piece can be coded and tested independently. We do not attemmpt to combine the subtasks into a complete task until each of the subtasks has been verified to work properly by itself.
The concept of top-down design is the basis of our formal program design process.
In [2]:
TikzPicture(L"""
\node (start) [draw, terminal, text width=3cm] {Start};
\node (problem) [draw, process, below of=start] {State the problem you are trying to solve};
\node (inout) [draw, process, below of=problem] {Define required inputs and outputs};
\node (algorithm) [draw, process, below of=inout] {Design the algorithm};
\node (code) [draw, process, below of=algorithm] {Convert the algorithm into high-level language statements};
\node (test) [draw, process, below of=code] {Test, debug and document the resulting code};
\node (finish) [draw, terminal, below of=test, text width=3cm] {Finished};
\node (decomposition) [draw, process, right of=algorithm, xshift=5cm, yshift=1cm] {Decompose into subtasks};
\node (stepwise) [draw, process, right of=algorithm, xshift=5cm, yshift=-1cm] {Stepwise refinement};
\draw [arrow] (start) -- (problem);
\draw [arrow] (problem) -- (inout);
\draw [arrow] (inout) -- (algorithm);
\draw [arrow] (algorithm) -- (code);
\draw [arrow] (code) -- (test);
\draw [arrow] (test) -- (finish);
\draw [arrow] (algorithm.north east) -- (decomposition.west);
\draw [arrow] (decomposition) -- (stepwise);
\draw [arrow] (stepwise.west) -- (algorithm.south east);
\draw [arrow] (decomposition.east) -- +(0.5,0) -- +(0.5,0.75) -- +(-2.65,0.75) -- (decomposition.north);
\draw [arrow] (stepwise.south) -- +(0,-0.5) -- +(3,-0.5) -- +(3,+0.3) -- (stepwise.east);
"""; options="very thick, scale=2, transform shape, node distance=1.5cm, text width=5cm, text centered", preamble="""
\\usepackage{newtxmath}
\\usepackage{flowchart}
\\usetikzlibrary{arrows}
\\tikzstyle{arrow} = [ultra thick,-latex]
\\renewcommand{\\familydefault}{\\sfdefault}
""")
Out[2]:
Programs are usually written to fill some perceived need, but that need may not be articulated clearly by the person requesting the program.
Eg. a user may ask for a program to solve a system of linear equations. This request in not clear enough to allow a programmer to design a program to meet the need; he or she must first know much more about the problem to be solved. Is the system of equations real or complex? What is the maximum number of equations that the program must handle? Are there any symmetries in the equations that might be exploited to make the task easier? The program designer will have to talk with the user requesting the program, and the two of them will have to come up with a clear statement of exactly what they are trying to accomplish. A clear statement of the problem will prevent misunderstandings, and it will also help the program designer to properly organize his or her thoughts;
Design and write a program to solve a system of linear equations having real coefficients and with up to 20 equations in 20 unknowns.
The inputs to the program and the outputs produced by the program must be specified so that the new program will properly fit into the overall processing scheme.
In the example, the coefficients of the equations to be solved are probably in some pre-existing order, and our new program needs to be able to read thme in that order. Similarly, it needs to produce the answer required by the programs that may follow in thet overall processing scheme, and to write out those answers in the format needed by the programs following it.
An algorithm is a step-by-step procedure for finding the solution to a problem. The designer looks for logical divisions within the problem, and divides it up into subtasks along those lines. This process is called decomposition. If the subtasks are themselves large, the designer can break them up into even smaller subtasks. This process continues until the problem has been divided into many small pieces, each of which does a simple, clearly understandable job.
After the problem has been decomposed into small pieces, each piece is further refined through a process called stepwise refinement. In this process, a designer starts with a general description of what the piece of code should do, and then defines in detail the procedure so that it is specific enough to be turned into high-level language statements. This is often done with flowcharts.
This step is the real killer. The components of the program must first be tested individually, if possible, and then the program as a whole must be tested. When testing a program, we must verify that it works correctly for all legal input data sets. If the algorithm implemented in a program includes different branches, we must test all of the possible branches to confirm that the program operates correctly under every possible circumstance.
The first stage of testing is called unit testing. During unit testing, the individual subtasks of the program are tested separately to confirm that they work correctly. The programmer usually writes small programs called stubs to execute the code under test, and to see if the code is returning the proper results. This verifies the operation of the subtasks at a basic level before they are combined into larger groups.
After the unit testing is completed, the program goes through a series of builds during which the individual subtasks are combined to produce the final program. The first build of the program typically includes only a few of the subtasks. It is used to check the interactions among those subtasks and the functions performed by the combination of the subtasks. In successive builds, more and more subtasks are addedn until the entire program is complete. Testing is performed on each build, and any errors (bugs) that are detected are corrected before moving on to the next build.
The first complete version of the program is called the alpha release. It is exercised by the programmer in as many ways as possible, and the bugs discovered during the testing are corrected.
When the most serious bugs have been removed from the program, a new version called the beta release is prepared. This release is normally given to users who have a need for the program. These users put the program through its paces under many different conditions and with many different input data sets, and they report any bugs that they find to the programmers. When those bugs have been corrected, the program is ready to be released for general use.
Comments inside a program are intended to help you and others understand program design and how it is organised. Program comments occur in the midst of the program's source and temporarily interrupt the highly restricted syntax of a high-level programming language. Comments are entirely ignored by the interpreter or compiler and are allowed to enhance program clarity for humans.
Not only comments are include in the source but also Docstrings that describe to a user how to use the program (function):
As a part of the design process, it is necessary to describe the algorithm that you intend to implement. This description should be in a standard form that is easy for both you and other people to understand, and the description should aid you in turning your concept in high-level language statements.
The standard forms that we use to describe the algorithms are called constructs, and an algorithm described using these constructs is called a structured algorithm. When the algorithm is implemented in a high-level language, the resulting program is called a structured program.
In [3]:
TikzPicture(L"""
\node (terminal)[draw, terminal, text width=2cm, text centered] {START STOP};
\node (io)[draw, io, below of=terminal, text width=2cm, text centered] {INPUT OUTPUT};
\node (process) [draw, process, below of=io, text width=3cm, text centered] {COMPUTATION};
\node (decision) [draw, decision, below of=process, text width=2cm, text centered] {DECISION};
\node (predproc) [draw, predproc, below of=decision, text width=2.3cm, text centered] {ALGORITHM};
\node (arrow) [below of=predproc, yshift=1cm] {};
\draw [arrow] (arrow) -- +(0, -1);
\node [text width=9cm, right of=terminal, xshift=5cm] {An oval indicates the start or stop of an algorithm};
\node [text width=9cm, right of=io, xshift=5cm] {A parallelogram indicates an input or output operation};
\node [text width=9cm, right of=process, xshift=5cm] {A rectangle indicates a computation, with the result assigned to a variable};
\node [text width=9cm, right of=decision, xshift=5cm] {A diamond indicates a point where a choice is made between two alternatives};
\node [text width=9cm, right of=predproc, xshift=5cm] {A double-lined rectangle indicates a reference to an elsewhere documented algorithm};
\node [text width=9cm, right of=arrow, xshift=5cm, yshift=-0.75cm] {An arrow indicates the direction of program flow between steps in the algorithm};
"""; options="very thick, scale=2, transform shape, node distance=1.5cm, minimum height=1cm", preamble="""
\\usepackage{newtxmath}
\\usepackage{flowchart}
\\usetikzlibrary{shapes.geometric, arrows}
\\tikzstyle{arrow} = [ultra thick,-latex]
\\tikzstyle{io} = [trapezium, trapezium left angle=70, trapezium right angle=110]
\\tikzstyle{decision} = [diamond, aspect=2]
\\renewcommand{\\familydefault}{\\sfdefault}
""")
Out[3]:
Eg, the addition of 2 floating point values is described by the following pseudo-code:
Compare the exponents of the two numbers; shift fraction of the smaller one to the right until its exponent would match the larger exponent.
Add the fractions.
Normalize the sum, either shifting right and incrementing the exponent or shifting left and decrementing the exponent.
Check overflow or underflow? If yes, generate an exception.
Round the fraction to the appriorate number of bits.
Is the result still normalized? If yes, we are done; if no go to step 3.
In [4]:
TikzPicture(L"""
\node (terminal)[draw, terminal, text width=2cm, text centered] {START};
\node (match) [draw, process, below of=terminal, text width=3cm, text centered] {MATCH\\ EXPONENTS};
\node (add) [draw, process, below of=match, text width=3cm, text centered] {ADD FRACTIONS};
\node (normal) [draw, process, below of=add, text width=3cm, text centered] {NORMALIZE};
\node (decision) [draw, decision, below of=normal, text width=2.5cm, text centered] {OVERFLOW UNDERFLOW};
\node (round) [draw, process, below of=decision, text width=3cm, text centered] {ROUND};
\node (normal2) [draw, decision, below of=round, text width=2.5cm, text centered] {NORMALIZED?};
\node (terminal2)[draw, terminal, below of=normal2, text width=2cm, text centered] {STOP};
\node (io)[draw, io, right of=decision, node distance=6cm, text width=2cm, text centered] {EXCEPTION};
\node [left of=normal2, yshift=0.5cm, xshift=-0.3cm]{NO};
\draw [arrow] (terminal) -- (match);
\draw [arrow] (match) -- (add);
\draw [arrow] (add) -- (normal);
\draw [arrow] (normal) -- (decision);
\draw [arrow] (decision) -- (round) node[midway, right] {NO};
\draw [arrow] (round) -- (normal2);
\draw [arrow] (normal2) -- (terminal2) node[midway, right] {YES};
\draw [arrow] (decision) -- (io) node[midway, above] {YES};
\draw [arrow] (normal2.west) -- +(-1,0) -- +(-1,6) -- (normal.west);
"""; options="very thick, scale=1.5, transform shape, node distance=2cm, minimum height=1cm", preamble="""
\\usepackage{newtxmath}
\\usepackage{flowchart}
\\usetikzlibrary{shapes.geometric, arrows}
\\tikzstyle{arrow} = [ultra thick,-latex]
\\tikzstyle{io} = [trapezium, trapezium left angle=70, trapezium right angle=110]
\\tikzstyle{decision} = [diamond, aspect=2]
\\renewcommand{\\familydefault}{\\sfdefault}
""")
Out[4]: